<?php
/**
 * Module: RandomQuote
 * 
 * This module provides functionality for some randomness. That description
 * might be random, but that's precisely the purpose of this module; being
 * random. Basically, what it does is monitoring a channel and storing the last
 * x messages in that channel (x can get configured). Then, after a random time
 * interval, the Timer gets called and one of those messages is repeated in the
 * channel. This can result in funny situations, which might liven up a dead
 * channel.
 * 
 * This module is completely configuration-less, so just plug it into your bot
 * and start spamming channels, but be sure to ask for the channel founder's
 * permission. I'm not responsible for your bot getting banned for annoying
 * behaviour.
 * 
 * @author Mr. Bondt <mrbondt@goldenak.eu>
 */
class RandomQuote extends ModuleBase
{
        
        /**
         * Array of channels where this module will be activated in.
         */
        private $m_aChannels;
        
        /**
         * This array contains the timestamp of the last message in every
         * channel.
         */
        private $m_aLastMessage;
        
        /**
         * This is timestamp of the last time the timer has run.
         */
        private $m_nLastRun;
        
        /**
         * Associative array with the last 100 messages of every channel this
         * module is active in.
         */
        private $m_aMessages;
        
        /**
         * The timer ID of the current active timer, if there is one.
         */
        private $m_sTimerId;
        
        /**
         * The number of messages we should store for each enabled channel.
         */
        private $m_nMessages;
        
        /**
         * The prefix for the Evaluation module. We save this here so we can
         * ignore message beginning with this.
         */
        private $m_sEvalPrefix;
        
        /**
         * The constructor readies the module for use.
         */
        public function __construct ()
        {
                if (!is_dir ('Data/RandomQuote'))
                {
                        mkdir ('Data/RandomQuote');
                }
                
                $aConf = Configuration :: getInstance () -> get ('Owners');
                $this -> m_sEvalPrefix = $aConf ['Prefix'];
                
                $this -> setNumMessages (100);
                $this -> load ();
                
                // Set up timer.
                $this -> setupTimer ();
        }
        
        /**
         * The destructor saves the until saved messages to file.
         */
        public function __destruct ()
        {
                $this -> save ();
        }
        
        /**
         * Loads up the channels where this module should be activated in.
         */
        public function load ()
        {
                // Convert the old format to the new format, assume the first
                // configured network.
                $aNetworks       = array_keys (Configuration :: getInstance () -> get ('Networks'));
                $sDefaultNetwork = $aNetworks [0];
                
                $this -> m_aChannels = array ();
                
                if (file_exists ('Data/RandomQuote/Channels.dat'))
                {
                        $this -> m_aChannels = unserialize (file_get_contents ('Data/RandomQuote/Channels.dat'));
                }
                
                foreach ($this -> m_aChannels as $iIndex => $sChannel)
                {
                        if (strpos ($sChannel, ':') !== false)
                        {
                                list ($sNetwork, $sChannel) = explode (':', $sChannel);
                        }
                        else
                        {
                                // Backward compatibility.
                                $sNetwork = $sDefaultNetwork;
                                
                                if (file_exists ('Data/RandomQuote/Messages_' . $sChannel . '.txt'))
                                {
                                        // Rename it
                                        rename (
                                                'Data/RandomQuote/Messages_' . $sChannel . '.txt',
                                                'Data/RandomQuote/Messages_' . $sNetwork . ',' . $sChannel . '.txt'
                                        );
                                }
                                
                                $this -> m_aChannels [$iIndex] = $sNetwork . ':' . $sChannel;
                        }
                        
                        $sFilename = 'Data/RandomQuote/Messages_' . $sNetwork . ',' . $sChannel . '.txt';
                        if (file_exists ($sFilename))
                        {
                                $this -> m_aMessages [$sNetwork][$sChannel] = file ($sFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                        }
                }
                
                $tsTime = time ();
                foreach ($this -> m_aChannels as $sChannel)
                {
                        $this -> m_aLastMessage [$sChannel] = $tsTime;
                        
                        // This should be fine now, as it's fixed in the previous loop.
                        list ($sNetwork, $sChannel) = explode (':', $sChannel);
                        
                        if (!isset ($this -> m_aMessages [$sNetwork]))
                        {
                                $this -> m_aMessages [$sNetwork] = array ();
                        }
                        
                        if (!isset ($this -> m_aMessages [$sNetwork][$sChannel]))
                        {
                                $this -> m_aMessages [$sNetwork][$sChannel] = array ();
                        }
                }
                
                // Save any corrections made.
                $this -> save ();
                
                // Prevents spamming in a channel after just starting.
                $this -> m_nLastRun = $tsTime;
        }
        
        /**
         * Saves the channels where this module should be used.
         */
        public function save ()
        {
                file_put_contents ('Data/RandomQuote/Channels.dat', serialize ($this -> m_aChannels));
                foreach ($this -> m_aChannels as $sChannel)
                {
                        list ($sNetwork, $sChannel) = explode (':', $sChannel);
                        file_put_contents (
                                'Data/RandomQuote/Messages_' . $sNetwork . ',' . $sChannel . '.txt',
                                implode (PHP_EOL, $this -> m_aMessages [$sNetwork][$sChannel])
                        );
                }
        }
        
        /**
         * Returns the number of messages we're storing to use in random
         * messages.
         * 
         * @return integer
         */
        public function getNumMessages ()
        {
                return $this -> m_nMessages;
        }
        
        /**
         * This method sets the number of messages we're storing for each 
         * channel, which can be used to spam a random quote once in a while.
         * 
         * @param integer $nMessages The number of messages to store.
         */
        public function setNumMessages ($nMessages)
        {
                $this -> m_nMessages = $nMessages;
        }
        
        /**
         * Enables this module for the given channel.
         * 
         * @param string $sNetwork The network on which this channel resides.
         * @param string $sChannel The channel to add.
         * @return boolean
         */
        public function addChannel ($sNetwork, $sChannel)
        {
                $sChannel = strtolower ($sChannel);
                if ($this -> isChannelAdded ($sNetwork, $sChannel))
                {
                        return false;
                }
                
                $this -> m_aChannels [] = $sNetwork . ':' . $sChannel;
                $this -> m_aMessages [$sNetwork][$sChannel] = array ();
                $this -> m_aLastMessage [$sNetwork . ':' . $sChannel] = 0;
                
                $this -> save ();
                return true;
        }
        
        /**
         * Disables this module for the given channel. If $bDelMessages is true
         * (default), the stored messages for the given channel will be deleted.
         * 
         * @param string $sNetwork The network on which this channel resides.
         * @param string $sChannel The channel to remove.
         * @param boolean $bDelMessages Whether to delete the stored messages.
         * @return boolean
         */
        public function removeChannel ($sNetwork, $sChannel, $bDelMessages = true)
        {
                $sChannel = strtolower ($sChannel);
                if (!$this -> isChannelAdded ($sNetwork, $sChannel))
                {
                        return false;
                }
                
                $bDone = false;
                foreach ($this -> m_aChannels as $nIndex => $sListedChannel)
                {
                        if ($sListedChannel == $sNetwork . ':' . $sChannel)
                        {
                                unset ($this -> m_aChannels [$nIndex]);
                                if ($bDelMessages)
                                {
                                        unset ($this -> m_aMessages [$sNetwork][$sChannel]);
                                }
                                $bDone = true;
                                break;
                        }
                }
                
                $this -> save ();
                return $bDone;
        }
        
        /**
         * Checks whether this module is active in the given channel.
         * 
         * @param string $sNetwork The network on which this channel resides.
         * @param string $sChannel The channel to check.
         * @return boolean
         */
        public function isChannelAdded ($sNetwork, $sChannel)
        {
                $sChannel = strtolower ($sChannel);
                
                return in_array ($sNetwork . ':' . $sChannel, $this -> m_aChannels);
        }
        
        /**
         * This method saves the message that has appeared in the given channel
         * for use as a possible random message. It'll also replace the bot's
         * own nickname with the nickname of the person who sent the message.
         * 
         * @param string $sNetwork The network on which this channel resides.
         * @param string $sChannel The channel where the message appeared in.
         * @param string $sMessage The message itself.
         * @param string $sOwnNickname The bot's own nickname.
         */
        private function addMessage ($sNetwork, $sChannel, $sNickname, $sMessage, $sOwnNickname)
        {
                $sChannel = strtolower ($sChannel);
                if (!$this -> isChannelAdded ($sNetwork, $sChannel))
                {
                        return;
                }
                
                if (strlen ($sMessage) > 1 && ($sMessage [0] == '!' || $sMessage [0] == '.'))
                {
                        // It's a command, skip it. It may very well be (or is) a command of this bot.
                        return;
                }
                
                if (substr ($sMessage, 0, strlen ($this -> m_sEvalPrefix)) == $this -> m_sEvalPrefix)
                {
                        // This is meant for the Evaluation module, skip it.
                        return;
                }
                
                // Replace bot's name with name of person who said it
                // First replace all caps bot's name with all caps person's name.
                $sMessage = str_replace (strtoupper ($sOwnNickname), strtoupper ($sNickname), $sMessage);
                // Also replace all lowercase, just to be perfect.
                $sMessage = str_replace (strtolower ($sOwnNickname), strtolower ($sNickname), $sMessage);
                // Not gonna bother with BrEeZeR talk.
                $sMessage = str_ireplace ($sOwnNickname, $sNickname, $sMessage);
                
                // We want unique messages in the buffer, but respond to non-unique messages as well.
                $this -> m_aLastMessage [$sNetwork . ':' . $sChannel] = time ();
                
                // Prevent adding the same messages (like when someone is spamming).
                // This is probably slow, but I don't care, since it works fine.
                if (in_array ($sMessage, $this -> m_aMessages [$sNetwork][$sChannel]))
                {
                        return;
                }
                
                // Add the message to the beginning, so we can shave the old messages of.
                array_unshift ($this -> m_aMessages [$sNetwork][$sChannel], $sMessage);
                
                if (count ($this -> m_aMessages [$sNetwork][$sChannel]) > $this -> m_nMessages)
                {
                        // Remove all messages above a certain index. This makes changes in the number of messages possible.
                        for ($i = $this -> m_nMessages; $i < count ($this -> m_aMessages [$sNetwork][$sChannel]); $i ++)
                        {
                                unset ($this -> m_aMessages [$sNetwork][$sChannel][$i]);
                        }
                }
        }
        
        /**
         * This method will setup the timer for the next random message. The
         * timer will always set up with a timeout between 7.5 and 15 minutes.
         */
        private function setupTimer ()
        {
                $this -> m_sTimerId = Timer :: Create
                (
                        array ($this, 'runTimer'),
                        mt_rand(450 , 900) * 1000,
                        false
                );
        }
        
        /**
         * This method will call the run() method and setup the timer for the
         * next round of randomness.
         */
        public function runTimer ()
        {
                $this -> save ();
                $this -> run ();
                $this -> setupTimer ();
        }
        
        /**
         * Spams every enabled channel with a random message.
         */
        public function run ()
        {
                $this -> save ();
                
                foreach ($this -> m_aChannels as $sChannel)
                {
                        list ($sNetwork, $sChannel) = explode (':', $sChannel);
                        
                        // Have there been messages since the last time we spammed?
                        if ($this -> m_aLastMessage [$sNetwork . ':' . $sChannel] < $this -> m_nLastRun)
                        {
                                continue;
                        }
                        
                        // Do we have enough messages to be serious?
                        if (count ($this -> m_aMessages [$sNetwork][$sChannel]) < 3)
                        {
                                continue;
                        }
                        
                        $pBot = BotManager :: getInstance () -> offsetGet ('network:' . $sNetwork . ' channel:' . $sChannel);
                        if ($pBot instanceof BotGroup)
                        {
                                if (count ($pBot) == 0)
                                {
                                        // No bots found.
                                        echo 'Could not find a bot in ' . $sChannel . ' on ' . $sNetwork . PHP_EOL;
                                        continue;
                                }
                                else
                                {
                                        $pBot = $pBot -> seek (0);
                                }
                        }
                        
                        // We start at index 2 because we don't want a repetition of one of the last messages.
                        $nIndex   = mt_rand (2, count ($this -> m_aMessages [$sNetwork][$sChannel]) - 1);
                        $sMessage = $this -> m_aMessages [$sNetwork][$sChannel][$nIndex];
                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :' . $sMessage);
                        
                        // Call Pisg logging module, if it exists.
                        if (class_exists ('Pisg'))
                        {
                                $pPisg = ModuleManager :: getInstance () -> offsetGet ('Pisg');
                                if ($pPisg instanceof ModuleBase)
                                {
                                        if (substr ($sMessage, 0, 8) == chr (1) . 'ACTION ')
                                        {
                                                $pPisg -> onCTCP ($pBot, $sChannel, $pBot ['Nickname'], 'ACTION', substr ($sMessage, 8, -1));
                                        }
                                        else
                                        {
                                                $pPisg -> onChannelPrivmsg ($pBot, $sChannel, $pBot ['Nickname'], $sMessage);
                                        }
                                }
                        }
                        
                        // Remove the message from the array, this prevents us from spammin' it again.
                        unset ($this -> m_aMessages [$sNetwork][$sChannel][$nIndex]);
                        
                        // Make sure the indexes are following eachother up so that we can still use mt_rand ().
                        $this -> m_aMessages [$sNetwork][$sChannel] = array_values ($this -> m_aMessages [$sNetwork][$sChannel]);
                }
                
                $this -> m_nLastRun = time ();
        }
        
        /**
         * Handles some basic commands for this module, but will mostly 
         * be storing messages that occured in a channel which this module
         * is active in.
         * 
         * @param Bot $pBot The bot which received the message.
         * @param string $sChannel The channel in which the message was received.
         * @param string $sNickname The nickname from whom we got the message.
         * @param string $sMessage The message itself.
         */
        public function onChannelPrivmsg (Bot $pBot, $sChannel, $sNickname, $sMessage)
        {
                // Handle commands.
                list ($sTrigger, $sParams) = Util:: parseMessage ($sMessage);
                
                if ($sMessage [0] == '!')
                {
                        switch ($sTrigger)
                        {
                                /**
                                 * Force an execution.
                                 */
                                case '!runrandom':
                                {
                                        $pEval = ModuleManager :: getInstance () -> offsetGet ('Evaluation');
                                        if (!$pEval -> checkSecurity ($pBot, 9999)) {
                                                // Not the (logged in) owner.
                                                return false;
                                        }
                                        
                                        $this -> run ();
                                        return;
                                }
                                
                                /**
                                 * Add a new channel which we're expected to spam.
                                 */
                                case '!addrandomchan':
                                {
                                        $pEval = ModuleManager :: getInstance () -> offsetGet ('Evaluation');
                                        if (!$pEval -> checkSecurity ($pBot, 9999))
                                        {
                                                return false;
                                        }
                                        
                                        if ($sParams == null || substr ($sParams, 0, 1) != '#')
                                        {
                                                $pBot -> send ('PRIVMSG ' . $sChannel . ' :10Usage: !addrandomchan Channel');
                                        }
                                        else
                                        {
                                                if ($this -> addChannel ($pBot ['Network'], $sParams))
                                                {
                                                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :3Success: Channel ' . $sParams . ' added.');
                                                }
                                                else
                                                {
                                                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :4Error: Channel ' . $sParams . ' is already added.');
                                                }
                                        }
                                        return;
                                }
                                
                                /**
                                 * People might get tired of this too. But that's okay, 
                                 * with this command one is able to remove the functionality.
                                 */
                                case '!delrandomchan':
                                case '!removerandomchan':
                                {
                                        $pEval = ModuleManager :: getInstance () -> offsetGet ('Evaluation');
                                        if (!$pEval -> checkSecurity ($pBot, 9999))
                                        {
                                                return false;
                                        }
                                        
                                        if ($sParams == null || substr ($sParams, 0, 1) != '#')
                                        {
                                                $pBot -> send ('PRIVMSG ' . $sChannel . ' :10Usage: !removerandomchan Channel');
                                        }
                                        else
                                        {
                                                if ($this -> removeChannel ($pBot ['Network'], $sParams))
                                                {
                                                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :3Success: Channel ' . $sParams . ' removed.');
                                                }
                                                else
                                                {
                                                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :4Error: Channel ' . $sParams . ' is not added.');
                                                }
                                        }
                                        return;
                                }
                                
                                /**
                                 * Just want to pause the module? That's cool, use this command.
                                 * Note that new messages aren't stored.
                                 * 
                                 * 22-11-2009: disabled since it just doesn't make sense without continuing to store messages.
                                 */
                                /*case '!pauserandom':
                                {	
                                        $pEval = ModuleManager :: getInstance () -> offsetGet ('Evaluation');
                                        if (!$pEval -> checkSecurity ($pBot, 9999))
                                        {
                                                return false;
                                        }
                                        
                                        if ($sParams == null || substr ($sParams, 0, 1) != '#')
                                        {
                                                $pBot -> send ('PRIVMSG ' . $sChannel . ' :10Usage: !pauserandom Channel');
                                        }
                                        else
                                        {
                                                if ($this -> removeChannel ($pBot ['Network'], $sParams, false))
                                                {
                                                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :3Success: Channel ' . $sParams . ' stopped.');
                                                }
                                                else
                                                {
                                                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :4Error: Channel ' . $sParams . ' is not added.');
                                                }
                                        }
                                        return;
                                }*/
                                
                                /**
                                 * Get an overview of all the channels we're currently active in.
                                 */
                                case '!listrandomchan':
                                {
                                        $sMessage = '10* RandomQuote module active in (' . count ($this -> m_aChannels) . '): ';
                                        foreach ($this -> m_aChannels as $sListedChannel)
                                        {
                                                $sMessage .= $sListedChannel . ', ';
                                        }
                                        
                                        $pBot -> send ('PRIVMSG ' . $sChannel . ' :' . substr ($sMessage, 0, -2));
                                        return;
                                }
                                
                                /**
                                 * Show some basic statistics.
                                 */
                                case '!randomstats':
                                {
                                        $sChannel = strtolower ($sChannel);
                                        
                                        if ($this -> isChannelAdded ($pBot ['Network'], $sChannel))
                                        {
                                                $pBot -> send
                                                (
                                                        'PRIVMSG ' . $sChannel . ' :10* Stats for ' . $sChannel . ': Number of messages: ' . 
                                                        count ($this -> m_aMessages [$pBot ['Network']][strtolower ($sChannel)]) . 
                                                        ' - Last message stored: ' . Util :: formatTime (time () - $this -> m_aLastMessage [$pBot ['Network'] . ':' . $sChannel], true) . ' ago'
                                                );
                                        }
                                        return;
                                }
                        }
                }
                
                if ($this -> isChannelAdded ($pBot ['Network'], $sChannel))
                {
                        // Add the received message to the list.
                        $this -> addMessage ($pBot ['Network'], $sChannel, $sNickname, $sMessage, $pBot ['Nickname']);
                }
        }
        
        /**
         * In order to catch the /me message as well, we're going to have to
         * parse the ACTION messages, because of a limitation in the Nuwani
         * framework. This will be done in this very method. The type ($sType)
         * we're looking for is ACTION.
         * 
         * @param Bot $pBot The bot which received the message.
         * @param string $sChannel The channel in which the message was received.
         * @param string $sNickname The nickname from whom we got the message.
         * @param string $sType The CTCP type.
         * @param string $sMessage The message itself.
         */
        public function onCTCP (Bot $pBot, $sChannel, $sNickname, $sType, $sMessage)
        {
                if (strtoupper ($sType) == 'ACTION' && $this -> isChannelAdded ($pBot ['Network'], $sChannel))
                {
                        $this -> addMessage ($pBot ['Network'], $sChannel, $sNickname, chr (1) . 'ACTION ' . $sMessage . chr (1), $pBot ['Nickname']);
                }
        }
}
?>